package xcli

import (
	"bytes"
	"encoding/json"
	"encoding/xml"
	"io/ioutil"
	"net/http"
	"net/url"
	"time"

	"gitee.com/xuesongtao/gotool/internal"
)

const (
	get uint8 = iota
	post

	ContentTypeJson     = "application/json"
	ContentTypeXml      = "application/xml"
	ContentTypeProtobuf = "application/octet-stream"

	defaultTimeOut = 15 * time.Second
)

// httpCliBodyer
type httpCliBodyer interface {
	marshal() ([]byte, error)
}

// 通用请求/响应(不需要结果的)参数 map
type BodyMap map[string]interface{}

func NewBodyMap(size int) BodyMap {
	return make(BodyMap, size)
}

func (r BodyMap) Set(key string, value interface{}) BodyMap {
	r[key] = value
	return r
}

func (r BodyMap) Marshal() ([]byte, error) {
	return json.Marshal(r)
}

func (r BodyMap) Unmarshal(resp []byte) (BodyMap, error) {
	err := json.Unmarshal(resp, &r)
	return r, err
}

// 获取格式化后的 json, 用于打印
func (r BodyMap) GetFormatJsonForPrint() string {
	b, _ := json.Marshal(r)
	var dst bytes.Buffer
	_ = json.Indent(&dst, b, "", "\t")
	return dst.String()
}

// ############################################## 实现 HttpCliBodyer 开始###########################################

// json 协议实现 HttpCliBodyer
type HttpCliJsonBody struct {
	body interface{}
}

func NewHttpCliJsonBody(body interface{}) *HttpCliJsonBody {
	return &HttpCliJsonBody{body: body}
}

func (j *HttpCliJsonBody) marshal() ([]byte, error) {
	if bodyBytes, ok := j.body.([]byte); !ok {
		if bytes, err := json.Marshal(j.body); err != nil {
			return nil, err
		} else {
			return bytes, nil
		}
	} else {
		return bodyBytes, nil
	}
}

// xml 协议实现 HttpCliBodyer
type HttpCliXmlBody struct {
	body interface{}
}

func NewHttpCliXmlBody(body interface{}) *HttpCliXmlBody {
	return &HttpCliXmlBody{body: body}
}

func (x *HttpCliXmlBody) marshal() ([]byte, error) {
	if bodyBytes, ok := x.body.([]byte); !ok {
		if bytes, err := xml.Marshal(x.body); err != nil {
			return nil, err
		} else {
			return bytes, nil
		}
	} else {
		return bodyBytes, nil
	}
}

// ############################################## 实现 HttpCliBodyer 结束###########################################

type HttpClientOption func(h *HttpCli)

// 设置过去时间
func WithHttpCliTimeOut(timeOut time.Duration) HttpClientOption {
	return func(h *HttpCli) {
		h.client.Timeout = timeOut
	}
}

// WithPrint 是否打印 log
func WithPrint(is bool) HttpClientOption {
	return func(h *HttpCli) {
		h.isPrint = is
	}
}

// WithProxy 设置代理
func WithProxy(uri *url.URL) HttpClientOption {
	return func(h *HttpCli) {
		h.client.Transport = &http.Transport{
			Proxy: http.ProxyURL(uri),
		}
	}
}

// HttpCli 会自己释放 Response.Body
// 并发安全由外部控制
type HttpCli struct {
	isPrint bool
	client  *http.Client
	header  map[string]string
	body    []byte
}

type HttpCliResp struct {
	StatusCode int
	Body       []byte
	Header     http.Header
}

func (h *HttpCliResp) Text() string {
	return string(h.Body)
}

func NewHttpCli(opts ...HttpClientOption) *HttpCli {
	obj := &HttpCli{
		isPrint: true,
		client: &http.Client{
			Timeout: defaultTimeOut,
		},
	}

	for _, opt := range opts {
		opt(obj)
	}
	return obj
}

func (h *HttpCli) SetHeader(key, value string) {
	if h.header == nil {
		h.header = make(map[string]string, 5)
	}
	h.header[key] = value
}

func (h *HttpCli) GetHeader(key string) string {
	return h.header[key]
}

func (h *HttpCli) HeaderIsExist(key string) bool {
	return h.GetHeader(key) != ""
}

func (h *HttpCli) Get(url string) (*HttpCliResp, error) {
	if h.isPrint {
		internal.Infof("method: GET, url: %s, header: %v", url, h.header)
	}
	return h.do(get, url)
}

// ReGet 重试
func (h *HttpCli) ReGet(url string, maxCount ...int) (*HttpCliResp, error) {
	count := 3
	if len(maxCount) > 0 {
		count = maxCount[0]
	}
	if h.isPrint {
		internal.Infof("method: GET, url: %s, header: %v", url, h.header)
	}
	for i := 0; i < count; i++ {
		resp, err := h.do(get, url)
		if err != nil {
			if h.isPrint {
				internal.Warningf("h.Get %q is failed, err: %v, it is retry [%d]", url, err, i)
			}
			continue
		}
		return resp, nil
	}
	return nil, nil
}

// 请求体为 []byte 数组, 不能打印请求体
func (h *HttpCli) Post(url string, body []byte, contentType ...string) (*HttpCliResp, error) {
	h.body = body
	if !h.HeaderIsExist("Content-Type") {
		if len(contentType) > 0 {
			h.SetHeader("Content-Type", contentType[0])
		} else {
			h.SetHeader("Content-Type", ContentTypeProtobuf)
		}
	}

	if h.isPrint {
		internal.Infof("method: POST, url: %s, header: %v", url, h.header)
	}
	return h.do(post, url)
}

// 请求体为 HttpCliBodyer, 支持多种请求协议, 默认按 protobuf 协议处理, 同时可以打印请求体
func (h *HttpCli) PostForHttpCliBodyer(url string, body httpCliBodyer) (*HttpCliResp, error) {
	var (
		bodyStr string // 用于打印日志
		err     error
	)
	contentType := ContentTypeProtobuf
	switch obj := body.(type) {
	case *HttpCliJsonBody: // json 协议
		if h.body, err = obj.marshal(); err != nil {
			return nil, err
		}

		contentType = ContentTypeJson
		bodyStr = string(h.body)
	case *HttpCliXmlBody: // xml 协议
		if h.body, err = obj.marshal(); err != nil {
			return nil, err
		}

		contentType = ContentTypeProtobuf
		bodyStr = string(h.body)
	}

	// 判断下是否设置 Content-Type
	if !h.HeaderIsExist("Content-Type") {
		h.SetHeader("Content-Type", contentType)
	}

	if h.isPrint {
		internal.Infof("method: POST, url: %s, header: %v, body: %s", url, h.header, bodyStr)
	}
	return h.do(post, url)
}

func (h *HttpCli) do(actionNum uint8, url string) (*HttpCliResp, error) {
	methodStr := "GET"
	switch actionNum {
	case get:
		methodStr = "GET"
	case post:
		methodStr = "POST"
	}
	request, err := http.NewRequest(methodStr, url, bytes.NewReader(h.body))
	if err != nil {
		return nil, err
	}

	for k, v := range h.header {
		request.Header.Set(k, v)
	}
	resp, err := h.client.Do(request)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	respByte, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	return &HttpCliResp{
		StatusCode: resp.StatusCode,
		Body:       respByte,
		Header:     resp.Header,
	}, nil
}

// =========================================封装常用的方法 ============================================

func Get(url string, headers map[string]string) (*HttpCliResp, error) {
	cli := NewHttpCli()
	for k, v := range headers {
		cli.SetHeader(k, v)
	}
	return cli.Get(url)
}

// 请求体为 []byte 数组, 不能打印请求体
func Post(url string, body []byte, headers map[string]string) (*HttpCliResp, error) {
	cli := NewHttpCli()
	for k, v := range headers {
		cli.SetHeader(k, v)
	}
	return cli.Post(url, body)
}

// 请求体为 HttpCliBodyer, 支持多种请求协议, 同时可以打印请求体
func PostForHttpCliBodyer(url string, body httpCliBodyer, headers map[string]string) (*HttpCliResp, error) {
	cli := NewHttpCli()
	for k, v := range headers {
		cli.SetHeader(k, v)
	}
	return cli.PostForHttpCliBodyer(url, body)
}
